package pulpfizz.physics;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;

import org.jbox2d.collision.AABB;
import org.jbox2d.collision.PolygonDef;
import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.dynamics.World;

import pulpcore.image.CoreImage;
import pulpfizz.pulp.body.BodySpriteOld;
import pulpfizz.pulp.body.ImageBodySprite;
import pulpfizz.pulp.body.Java2DBodySprite;
import pulpfizz.pulp.body.PhysicsLayer;

public class PhysicsTileMap {
	private HashMap<Character, BodyCreatorFunction> bodyMap;
	private HashMap<Character, CoreImage> imageMap;
	
	private ArrayList<Body> bodies;
	private ArrayList<BodySpriteOld> bodySprites;
	
	/* 
	 * The following variables are associated with the physical
	 * instantiation of the tile map.  
	 */
	
	/** World AABB of the tile map. */
	private AABB aabb;
	private World world;
	private PhysicsLayer physics;
	
	public PhysicsTileMap() {
		bodyMap = new HashMap<Character,BodyCreatorFunction>();
		imageMap = new HashMap<Character,CoreImage>();
		bodies = new ArrayList<Body>();
		bodySprites = new ArrayList<BodySpriteOld>();
	}
	
	/**
	 * Binds a function (accessed by passing the object and the
	 * function name) to the character
	 * for use in creating objects for the tile map.
	 * <BR><BR>
	 * This will call the function via reflection; you may
	 * also create your own implementations of the BodyCreatorFunction
	 * interface if you prefer not to rely on reflection.
	 * @param c The char to bind this function to
	 * @param object The object whose method you should call
	 * @param methodName The name of the method to call.  This method
	 * must take (AABB, World) as parameters and return the ArrayList<Body of
	 * Bodies that it creates (it is reasonably expected that it will
	 * also add the bodies to the world).
	 */
	@SuppressWarnings("unchecked")
	public void bindBodyCreationFunction(char c, final Object object, final String methodName) {
		final PhysicsTileMap parent = this;
		
		BodyCreatorFunction bcf = new BodyCreatorFunction(){
			public Object o;
			public Method m;
			public PhysicsTileMap tileMap;
			
			{
				o = object;
				tileMap = parent;
				try {
					Class myClass = o.getClass();
					Class[] parameterTypes = new Class[]{AABB.class,World.class};
					m = myClass.getMethod(methodName, parameterTypes);
				} catch (Exception e) {
					//e.printStackTrace();
					System.err.println("Error registering "+methodName+" for class "+o.getClass()+": "+o);
					throw new RuntimeException();
				}
			}
			
			public ArrayList<Body> create(AABB p, World w) {
				ArrayList<Body> returnBodies = null;
				try {
					returnBodies = (ArrayList<Body>)m.invoke(o,p,w);
				} catch (Exception e) {
					//e.printStackTrace();
					System.err.println("Error calling "+m.getName());
					throw new RuntimeException();
				}				
				tileMap.bodies.addAll(returnBodies);
				return returnBodies;
			}
		};
		
		bodyMap.put(c, bcf);		
	}
	
	public void bindBodyCreationFunction(char c, BodyCreatorFunction bcf) {
		bodyMap.put(c, bcf);
	}
	
	public void bindCoreImage(char c, CoreImage im) {
		imageMap.put(c, im);
	}
	
	public void bindImageFromFile(char c, String fileName) {
		imageMap.put(c, CoreImage.load(fileName));        
	}
	
	public AABB setAABBFromMap(String[] map, 
							float startX, float startY,
							float tileW, float tileH) {
		int mapW = 0;
		int mapH = map.length;
		for (int i=0; i<mapH; ++i) {
			mapW = Math.max(mapW,map[i].length());
		}
		
		float endX = startX + mapW*tileW;
		float endY = startY + mapH*tileH;
		
		float minX = Math.min(startX, endX);
		float maxX = Math.max(startX, endX);
		float minY = Math.min(startY, endY);
		float maxY = Math.max(startY, endY);
		
		AABB result = new AABB();
		result.lowerBound.set(minX,minY);
		result.upperBound.set(maxX,maxY);
		
		aabb = result;
		
		return result;
	}
	
	public void setWorld(World w) {
		world = w;
	}
	
	public World getWorld() {
		return world;
	}
	
	public void setPhysicsLayer(PhysicsLayer p) {
		physics = p;
	}
	
	public PhysicsLayer getPhysics() {
		return physics;
	}
	
	public ArrayList<BodySpriteOld> createTileMap(String[] imageTiles, String[] bodyTiles) {
		return createTileMap(imageTiles, bodyTiles, aabb, world, physics);
	}
	
	public ArrayList<BodySpriteOld> createTileMap(
					String[] imageTiles, String[] bodyTiles,
					AABB boundingBox,
					World _world, PhysicsLayer _physics) {
		
		/*
		 * TODO ewj: set this up so that we can embed tile maps
		 * within each other.  Right now each tile map always adds
		 * its BodySprites to the physics layer, and draws a default
		 * picture based on geometry if none exists already.  We should add a
		 * flag to turn this default behavior and just leave things
		 * alone, or even better, provide a mechanism to automatically
		 * determine whether the tiles have created their own geometry.
		 * 
		 * Might require some refactoring of the way the callbacks work...
		 */
	
		String errorStr = "Image tilemap and body tilemap must have same dimensions.";
		if (imageTiles.length != bodyTiles.length) {
			throw new IllegalArgumentException(errorStr);
		}
		
		aabb = boundingBox;
		world = _world;
		physics = _physics;
		
		int nRows = imageTiles.length;
		int nCols = 0;
		
		for (int i=0; i<nRows; ++i) {
			if (imageTiles[i].length() != bodyTiles[i].length()) {
				throw new IllegalArgumentException(errorStr);
			}
			if (imageTiles[i].length() > nCols) nCols = imageTiles[i].length();
		}
		
		if (nRows == 0 || nCols == 0) throw new IllegalArgumentException("Number of rows and columns must be greater than zero.");
		
		float rowHeight = (aabb.upperBound.y - aabb.lowerBound.y) / nRows;
		float colWidth = (aabb.upperBound.x - aabb.lowerBound.x) / nCols;
		
		bodies = new ArrayList<Body>();
		bodySprites = new ArrayList<BodySpriteOld>();
		ArrayList<BodySpriteOld> bgSprites = new ArrayList<BodySpriteOld>();
		
		for (int i=0; i<nRows; ++i) {
			float startX = aabb.lowerBound.x;
			float startY = aabb.upperBound.y-rowHeight*i;
			
			for (int j=0; j<imageTiles[i].length(); ++j) {
				AABB toPass = new AABB(new Vec2(startX+colWidth*j,startY-rowHeight),
						   			   new Vec2(startX+colWidth*(j+1),startY));
				CoreImage im = imageMap.get(imageTiles[i].charAt(j));
				BodyCreatorFunction bcf = bodyMap.get(bodyTiles[i].charAt(j));
				ArrayList<Body> created = null;
				if (bcf != null) {
					created = bcf.create(toPass, world);
					bodies.addAll(created);
				}
				
				if (bcf == null || created == null || created.size() == 0) {
					/* 
					 * No geometry has been created, but there
					 * still might be an image to add.  If this
					 * is the case, we add it to the ground body,
					 * as it is probably background geometry. 
					 */
					if (im != null) {
						ImageBodySprite spr = new ImageBodySprite(world.getGroundBody(),physics,im,toPass);
						bgSprites.add(spr);
					}
					continue;
				}
				
				Body toAddToSprite = created.get(0);
				
				if (im != null) {
					ImageBodySprite spr = new ImageBodySprite(toAddToSprite,physics,im);
					bodySprites.add(spr);
				} else {
					Java2DBodySprite spr2 = new Java2DBodySprite(toAddToSprite,physics);
					bodySprites.add(spr2);
				}
			}
		}
		// Make sure to add the (always static) background sprites first so
		// they don't draw on top of the possibly dynamic ones.
		for (BodySpriteOld bs: bgSprites) {
			physics.add(bs);
		}
		for (BodySpriteOld bs: bodySprites) {
        	physics.add(bs);
        }
		for (BodySpriteOld bs: bgSprites) {
			bodySprites.add(bs);
		}
		return bodySprites;
	}
	
	
	/**
	 * Makes an extremely basic tile map creator.
	 * <BR><BR>
	 * Tiles:
	 * O - a circle
	 * | - slanting triangle (filled bottom) - should be \, but that messes up string alignment
	 * / - slanting triangle (filled bottom)
	 * # - filled block
	 * B - movable square
	 * (space)  - empty space
	 * 
	 * @return A simple example tile map
	 */
	static public PhysicsTileMap createSimpleTileMap() {
		PhysicsTileMap result = new PhysicsTileMap();

		@SuppressWarnings("unused")
		Object functionHolder = new Object() {
			
			public ArrayList<Body> createO(AABB p, World w) {
				ArrayList<Body> bodyList = new ArrayList<Body>();
				Vec2 center = p.upperBound.add(p.lowerBound);
				center.mulLocal(0.5f);
				float radius = 0.5f*(p.upperBound.x - p.lowerBound.x);
				Body circle = BodyUtils.createCircle(w, center.x, center.y, radius);
				bodyList.add(circle);
				return bodyList;
			}
			public ArrayList<Body> createSlashLeft(AABB p, World w) {
				ArrayList<Body> bodyList = new ArrayList<Body>();
				PolygonDef pd = new PolygonDef();
				pd.vertices.add(p.lowerBound);
				pd.vertices.add(new Vec2(p.upperBound.x,p.lowerBound.y));
				pd.vertices.add(new Vec2(p.lowerBound.x,p.upperBound.y));
				BodyDef bd = new BodyDef();
				Body slashLeft = w.createBody(bd);
				slashLeft.createShape(pd);
				bodyList.add(slashLeft);
				return bodyList;
			}
			public ArrayList<Body> createSlashRight(AABB p, World w) {
				ArrayList<Body> bodyList = new ArrayList<Body>();
				PolygonDef pd = new PolygonDef();
				pd.vertices.add(p.lowerBound);
				pd.vertices.add(new Vec2(p.upperBound.x,p.lowerBound.y));
				pd.vertices.add(p.upperBound);
				BodyDef bd = new BodyDef();
				Body slashRight = w.createBody(bd);
				slashRight.createShape(pd);
				bodyList.add(slashRight);
				return bodyList;
			}
			public ArrayList<Body> createPound(AABB p, World w) {
				ArrayList<Body> bodyList = new ArrayList<Body>();
				PolygonDef pd = new PolygonDef();
				pd.vertices.add(p.lowerBound);
				pd.vertices.add(new Vec2(p.upperBound.x,p.lowerBound.y));
				pd.vertices.add(p.upperBound);
				pd.vertices.add(new Vec2(p.lowerBound.x,p.upperBound.y));
				BodyDef bd = new BodyDef();
				Body pound = w.createBody(bd);
				pound.createShape(pd);
				bodyList.add(pound);
				return bodyList;
			}
			
			/** A plain old box that fills the AABB. */
			public ArrayList<Body> createB(AABB p, World w) {
				ArrayList<Body> bodyList = new ArrayList<Body>();
				PolygonDef pd = new PolygonDef();
				pd.vertices.add(p.lowerBound);
				pd.vertices.add(new Vec2(p.upperBound.x,p.lowerBound.y));
				pd.vertices.add(p.upperBound);
				pd.vertices.add(new Vec2(p.lowerBound.x,p.upperBound.y));
				pd.density = 0.1f;
				BodyDef bd = new BodyDef();
				Body bbody = w.createBody(bd);
				bbody.createShape(pd);
				bbody.setMassFromShapes();
				bodyList.add(bbody);
				return bodyList;
			}
			
			public ArrayList<Body> createNothing(AABB p, World w) {
				return new ArrayList<Body>();
			}
		};
		
		// Use reflection binding method
		result.bindBodyCreationFunction('O', functionHolder, "createO");
		result.bindBodyCreationFunction('|', functionHolder, "createSlashLeft");
		result.bindBodyCreationFunction('/', functionHolder, "createSlashRight");
		result.bindBodyCreationFunction('#', functionHolder, "createPound");
		result.bindBodyCreationFunction('B', functionHolder, "createB");
		result.bindBodyCreationFunction(' ', functionHolder, "createNothing");
		
		return result;
	}
	
}

